let usedOnStart = 0;
let enabled = false;
let depth = 0;

declare global {
	interface Game {
		profiler,
	}

	interface Memory {
		profiler,
	}

	namespace NodeJS {
		interface Global {
			performance,
		}
	}
}

function setupProfiler() {
	depth = 0; // reset depth, this needs to be done each tick.
	Game.profiler = {
		stream(duration, filter) {
			setupMemory('stream', duration || 10, filter);
		},
		email(duration, filter) {
			setupMemory('email', duration || 100, filter);
		},
		profile(duration, filter) {
			setupMemory('profile', duration || 100, filter);
		},
		background(filter) {
			setupMemory('background', false, filter);
		},
		restart() {
			if (Profiler.isProfiling()) {
				const filter = Memory.profiler.filter;
				let duration: boolean | number = false;
				if (!!Memory.profiler.disableTick) {
					// Calculate the original duration, profile is enabled on the tick after the first call,
					// so add 1.
					duration = Memory.profiler.disableTick - Memory.profiler.enabledTick + 1;
				}
				const type = Memory.profiler.type;
				setupMemory(type, duration, filter);
			}
		},
		reset: resetMemory,
		output: Profiler.output,
	};

	overloadCPUCalc();
}

function setupMemory(profileType, duration, filter) {
	resetMemory();
	const disableTick = Number.isInteger(duration) ? Game.time + duration : false;
	if (!Memory.profiler) {
		Memory.profiler = {
			map: {},
			totalTime: 0,
			enabledTick: Game.time + 1,
			disableTick,
			type: profileType,
			filter,
		};
	}
}

function resetMemory() {
	Memory.profiler = null;
}

function overloadCPUCalc() {
	if (Game.rooms.sim) {
		usedOnStart = 0; // This needs to be reset, but only in the sim.
		Game.cpu.getUsed = function getUsed() {
			return global.performance.now() - usedOnStart;
		};
	}
}

function getFilter() {
	return Memory.profiler.filter;
}

const functionBlackList = [
	'getUsed', // Let's avoid wrapping this... may lead to recursion issues and should be inexpensive.
	'constructor', // es6 class constructors need to be called with `new`
];

function wrapFunction(name, originalFunction) {
	return function wrappedFunction() {
		if (Profiler.isProfiling()) {
			const nameMatchesFilter = name === getFilter();
			const start = Game.cpu.getUsed();
			if (nameMatchesFilter) {
				depth++;
			}
			const result = originalFunction.apply(this, arguments);
			if (depth > 0 || !getFilter()) {
				const end = Game.cpu.getUsed();
				Profiler.record(name, end - start);
			}
			if (nameMatchesFilter) {
				depth--;
			}
			return result;
		}

		return originalFunction.apply(this, arguments);
	};
}

function hookUpPrototypes() {
	Profiler.prototypes.forEach(proto => {
		profileObjectFunctions(proto.val, proto.name);
	});
}

function profileObjectFunctions(object, label) {
	const objectToWrap = object.prototype ? object.prototype : object;

	Object.getOwnPropertyNames(objectToWrap).forEach(functionName => {
		const extendedLabel = `${label}.${functionName}`;
		try {
			const isFunction = typeof objectToWrap[functionName] === 'function';
			const notBlackListed = functionBlackList.indexOf(functionName) === -1;
			if (isFunction && notBlackListed) {
				const originalFunction = objectToWrap[functionName];
				objectToWrap[functionName] = profileFunction(originalFunction, extendedLabel);
			}
		} catch (e) { } /* eslint no-empty:0 */
	});

	return objectToWrap;
}

function profileFunction(fn, functionName) {
	const fnName = functionName || fn.name;
	if (!fnName) {
		console.log('Couldn\'t find a function name for - ', fn);
		console.log('Will not profile this function.');
		return fn;
	}

	return wrapFunction(fnName, fn);
}

const Profiler = {
	printProfile() {
		console.log(Profiler.output());
	},

	emailProfile() {
		Game.notify(Profiler.output());
	},

	output(numresults?: number) {
		const displayresults = !!numresults ? numresults : 20;
		if (!Memory.profiler || !Memory.profiler.enabledTick) {
			return 'Profiler not active.';
		}

		const elapsedTicks = Game.time - Memory.profiler.enabledTick + 1;
		const header = 'calls\t\ttime\t\tavg\t\tfunction';
		const footer = [
			`Avg: ${(Memory.profiler.totalTime / elapsedTicks).toFixed(2)}`,
			`Total: ${Memory.profiler.totalTime.toFixed(2)}`,
			`Ticks: ${elapsedTicks}`,
		].join('\t');
		return [].concat(header, Profiler.lines().slice(0, displayresults), footer).join('\n');
	},

	lines() {
		const stats = Object.keys(Memory.profiler.map).map(functionName => {
			const functionCalls = Memory.profiler.map[functionName];
			return {
				name: functionName,
				calls: functionCalls.calls,
				totalTime: functionCalls.time,
				averageTime: functionCalls.time / functionCalls.calls,
			};
		}).sort((val1, val2) => {
			return val2.totalTime - val1.totalTime;
		});

		const lines = stats.map(data => {
			return [
				data.calls,
				data.totalTime.toFixed(1),
				data.averageTime.toFixed(3),
				data.name,
			].join('\t\t');
		});

		return lines;
	},

	prototypes: [
		{ name: 'Game', val: Game },
		{ name: 'Room', val: Room },
		{ name: 'Structure', val: Structure },
		{ name: 'Spawn', val: Spawn },
		{ name: 'Creep', val: Creep },
		{ name: 'RoomPosition', val: RoomPosition },
		{ name: 'Source', val: Source },
		{ name: 'Flag', val: Flag },
	],

	record(functionName, time) {
		if (!Memory.profiler.map[functionName]) {
			Memory.profiler.map[functionName] = {
				time: 0,
				calls: 0,
			};
		}
		Memory.profiler.map[functionName].calls++;
		Memory.profiler.map[functionName].time += time;
	},

	endTick() {
		if (Game.time >= Memory.profiler.enabledTick) {
			const cpuUsed = Game.cpu.getUsed();
			Memory.profiler.totalTime += cpuUsed;
			Profiler.report();
		}
	},

	report() {
		if (Profiler.shouldPrint()) {
			Profiler.printProfile();
		} else if (Profiler.shouldEmail()) {
			Profiler.emailProfile();
		}
	},

	isProfiling() {
		if (!enabled || !Memory.profiler) {
			return false;
		}
		return !Memory.profiler.disableTick || Game.time <= Memory.profiler.disableTick;
	},

	type() {
		return Memory.profiler.type;
	},

	shouldPrint() {
		const streaming = Profiler.type() === 'stream';
		const profiling = Profiler.type() === 'profile';
		const onEndingTick = Memory.profiler.disableTick === Game.time;
		return streaming || (profiling && onEndingTick);
	},

	shouldEmail() {
		return Profiler.type() === 'email' && Memory.profiler.disableTick === Game.time;
	},
};

export default {
	wrap(callback) {
		if (enabled) {
			setupProfiler();
		}

		if (Profiler.isProfiling()) {
			usedOnStart = Game.cpu.getUsed();

			// Commented lines are part of an on going experiment to keep the profiler
			// performant, and measure certain types of overhead.

			// var callbackStart = Game.cpu.getUsed();
			const returnVal = callback();
			// var callbackEnd = Game.cpu.getUsed();
			Profiler.endTick();
			// var end = Game.cpu.getUsed();

			// var profilerTime = (end - start) - (callbackEnd - callbackStart);
			// var callbackTime = callbackEnd - callbackStart;
			// var unaccounted = end - profilerTime - callbackTime;
			// console.log('total-', end, 'profiler-', profilerTime, 'callbacktime-',
			// callbackTime, 'start-', start, 'unaccounted', unaccounted);
			return returnVal;
		}

		return callback();
	},

	enable() {
		enabled = true;
		hookUpPrototypes();
	},

	output: Profiler.output,

	registerObject: profileObjectFunctions,
	registerFN: profileFunction,
	registerClass: profileObjectFunctions,
};
